layout.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. import Image from "next/image";
  2. import { Inter, Permanent_Marker } from "next/font/google";
  3. import { GeistSans } from "geist/font/sans";
  4. import { GeistMono } from "geist/font/mono";
  5. import { cn } from "@/shared/lib/utils";
  6. import { getServerUrl } from "@/shared/lib/server-url";
  7. import { FB_PIXEL_ID } from "@/shared/lib/facebook/fb-pixel";
  8. import { SiteConfig } from "@/shared/config/site-config";
  9. import { Header } from "@/features/layout/Header";
  10. import { Footer } from "@/features/layout/Footer";
  11. import { TailwindIndicator } from "@/components/utils/TailwindIndicator";
  12. import { NextTopLoader } from "@/components/ui/next-top-loader";
  13. import FacebookPixel from "@/components/FacebookPixel";
  14. import { Providers } from "./providers";
  15. import type { ReactElement } from "react";
  16. import type { Metadata } from "next";
  17. import "@/shared/styles/globals.css";
  18. export const metadata: Metadata = {
  19. title: {
  20. default: SiteConfig.title,
  21. template: `%s | ${SiteConfig.title}`,
  22. },
  23. description: SiteConfig.description,
  24. metadataBase: new URL(getServerUrl()),
  25. robots: {
  26. index: true,
  27. follow: true,
  28. googleBot: {
  29. index: true,
  30. follow: true,
  31. "max-snippet": -1,
  32. "max-image-preview": "large",
  33. "max-video-preview": -1,
  34. },
  35. },
  36. openGraph: {
  37. title: SiteConfig.title,
  38. description: SiteConfig.description,
  39. url: getServerUrl(),
  40. siteName: SiteConfig.title,
  41. images: [
  42. {
  43. url: `${getServerUrl()}/images/default-og-image_fr.png`,
  44. width: 1200,
  45. height: 630,
  46. alt: SiteConfig.title,
  47. },
  48. {
  49. url: `${getServerUrl()}/images/default-og-image_en.png`,
  50. width: 1200,
  51. height: 630,
  52. alt: SiteConfig.title,
  53. },
  54. ],
  55. locale: "fr_FR",
  56. type: "website",
  57. },
  58. twitter: {
  59. card: "summary_large_image",
  60. site: "@workout_cool",
  61. title: SiteConfig.title,
  62. description: SiteConfig.description,
  63. images: [`${getServerUrl()}/images/default-og-image_fr.png`],
  64. },
  65. alternates: {
  66. canonical: "https://www.workout.cool",
  67. languages: {
  68. fr: "https://www.workout.cool/fr",
  69. en: "https://www.workout.cool/en",
  70. },
  71. },
  72. authors: [{ name: "Workout Cool", url: "https://www.workout.cool" }],
  73. icons: {
  74. icon: [
  75. { url: "/images/favicon-32x32.png", sizes: "32x32", type: "image/png" },
  76. { url: "/images/favicon-16x16.png", sizes: "16x16", type: "image/png" },
  77. { url: "/images/favicon.ico", type: "image/x-icon" },
  78. ],
  79. apple: "/apple-touch-icon.png",
  80. },
  81. manifest: "/site.webmanifest",
  82. };
  83. const inter = Inter({
  84. subsets: ["latin"],
  85. variable: "--font-inter",
  86. display: "swap",
  87. });
  88. const permanentMarker = Permanent_Marker({
  89. weight: "400",
  90. subsets: ["latin"],
  91. variable: "--font-permanent-marker",
  92. display: "swap",
  93. });
  94. export const preferredRegion = ["fra1", "sfo1", "iad1"];
  95. interface RootLayoutProps {
  96. params: Promise<{ locale: string }>;
  97. children: ReactElement;
  98. }
  99. export default async function RootLayout({ params, children }: RootLayoutProps) {
  100. const { locale } = await params;
  101. return (
  102. <>
  103. <html className="h-full" dir="ltr" lang={locale} suppressHydrationWarning>
  104. <head>
  105. <meta charSet="UTF-8" />
  106. <meta content="#f3f4f6" media="(prefers-color-scheme: light)" name="theme-color" />
  107. <meta content="#18181b" media="(prefers-color-scheme: dark)" name="theme-color" />
  108. <meta content="width=device-width, initial-scale=1, maximum-scale=1 viewport-fit=cover" name="viewport" />
  109. <link href="/site.webmanifest" rel="manifest" />
  110. {/* eslint-disable-next-line @next/next/no-page-custom-font */}
  111. <link as="style" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="preload" />
  112. {/* Alternate hreflang for i18n */}
  113. <link href="https://www.workout.cool/fr" hrefLang="fr" rel="alternate" />
  114. <link href="https://www.workout.cool/en" hrefLang="en" rel="alternate" />
  115. {/* TODO: maybe add some ads ? */}
  116. <noscript>
  117. <Image
  118. alt="Facebook Pixel"
  119. height="1"
  120. src={`https://www.facebook.com/tr?id=${FB_PIXEL_ID}&ev=PageView&noscript=1`}
  121. style={{ display: "none" }}
  122. width="1"
  123. />
  124. </noscript>
  125. </head>
  126. <body
  127. className={cn(
  128. "flex flex-col justify-between items-center p-8 min-h-screen max-sm:p-0 max-sm:min-h-full text-sm/[22px] font-normal text-base-content bg-base-200 dark:bg-[#18181b] dark:text-gray-200 antialiased",
  129. "bg-hero-light dark:bg-hero-dark",
  130. GeistMono.variable,
  131. GeistSans.variable,
  132. inter.variable,
  133. permanentMarker.variable,
  134. )}
  135. suppressHydrationWarning
  136. >
  137. <Providers locale={locale}>
  138. {/* <WorkoutSessionsSynchronizer /> */}
  139. <NextTopLoader color="#FF5722" delay={100} showSpinner={false} />
  140. {/* Main Card Container */}
  141. <div className="card bg-base-100 dark:bg-[#232324] shadow-xl w-full max-w-3xl max-sm:rounded-none max-sm:h-full border border-base-200 dark:border-gray-800">
  142. <div className="card-body p-0">
  143. <Header />
  144. <div className="px-2 sm:px-6 pb-6">{children}</div>
  145. </div>
  146. <Footer />
  147. </div>
  148. <TailwindIndicator />
  149. <FacebookPixel />
  150. </Providers>
  151. </body>
  152. </html>
  153. </>
  154. );
  155. }